﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.IO;

namespace BitcoinLibrary
{
    /**
     * BitCoin transactions don't specify what they do directly. Instead <a href="https://en.bitcoin.it/wiki/Script">a
     * small binary stack language</a> is used to define programs that when evaluated return whether the transaction
     * "accepts" or rejects the other transactions connected to it.<p>
     *
     * This implementation of the scripting language is incomplete. It contains enough support to run standard
     * transactions generated by the official client, but non-standard transactions will fail.
     */
    public class Script
    {
        //private static Logger log = LoggerFactory.getLogger(Script.class);

        // Some constants used for decoding the scripts.
        public const int OP_PUSHDATA1 = 76;
        public const int OP_PUSHDATA2 = 77;
        public const int OP_PUSHDATA4 = 78;
        public const int OP_DUP = 118;
        public const int OP_HASH160 = 169;
        public const int OP_EQUALVERIFY = 136;
        public const int OP_CHECKSIG = 172;

        byte[] program;
        private int cursor;

        // The stack consists of an ordered series of data buffers growing from zero up.
        private readonly Stack<byte[]> stack;
        // The program is a set of byte[]s where each element is either [opcode] or [data, data, data ...]
        private List<byte[]> chunks;
        private bool tracing;
        byte[] programCopy;      // TODO: remove this
        private readonly NetworkParameters param;

        /** Concatenates two scripts to form a new one. This is used when verifying transactions. */
        public static Script join(Script a, Script b)
        {
            Debug.Assert(a.param == b.param);
            byte[] fullProg = new byte[a.programCopy.Length + b.programCopy.Length];
            Array.Copy(a.programCopy, 0, fullProg, 0, a.programCopy.Length);
            Array.Copy(b.programCopy, 0, fullProg, a.programCopy.Length, b.programCopy.Length);
            return new Script(a.param, fullProg, 0, fullProg.Length);
        }

        /**
         * Construct a Script using the given network parameters and a range of the programBytes array.
         * @param params Network parameters.
         * @param programBytes Array of program bytes from a transaction.
         * @param offset How many bytes into programBytes to start reading from.
         * @param length How many bytes to read.
         * @throws ScriptException
         */
        public Script(NetworkParameters param, byte[] programBytes, int offset, int length)
        {
            this.param = param;
            stack = new Stack<byte[]>();
            parse(programBytes, offset, length);
        }

        /** If true, running a program will log its instructions. */
        public void setTracing(bool value)
        {
            this.tracing = value;
        }

        /** Returns the program opcodes as a string, for example "[1234] DUP HAHS160" */
        public override string ToString()
        {
            StringBuilder buf = new StringBuilder();
            foreach (byte[] chunk in chunks)
            {
                if (chunk.Length == 1)
                {
                    String opName;
                    int opcode = 0xFF & chunk[0];
                    switch (opcode)
                    {
                        case OP_DUP:
                            opName = "DUP";
                            break;
                        case OP_HASH160:
                            opName = "HASH160";
                            break;
                        case OP_CHECKSIG:
                            opName = "CHECKSIG";
                            break;
                        case OP_EQUALVERIFY:
                            opName = "EQUALVERIFY";
                            break;
                        default:
                            opName = "?(" + opcode + ")";
                            break;
                    }
                    buf.Append(opName);
                    buf.Append(" ");
                }
                else
                {
                    // Data chunk
                    buf.Append("[");
                    buf.Append(chunk.Length);
                    buf.Append("]");
                    buf.Append(Utils.bytesToHexString(chunk));
                    buf.Append(" ");
                }
            }
            return buf.ToString();
        }


        private byte[] getData(int len)
        {
            try
            {
                byte[] buf = new byte[len];
                Array.Copy(program, cursor, buf, 0, len);
                cursor += len;
                return buf;
            }
            catch (Exception e)
            {
                // We want running out of data in the array to be treated as a handleable script parsing exception,
                // not something that abnormally terminates the app.
                throw new Exception("Failed read of " + len + " bytes", e);
            }
        }

        private int readByte()
        {
            return 0xFF & program[cursor++];
        }

        /**
         * To run a script, first we parse it which breaks it up into chunks representing pushes of
         * data or logical opcodes. Then we can run the parsed chunks.
         *
         * The reason for this split, instead of just interpreting directly, is to make it easier
         * to reach into a programs structure and pull out bits of data without having to run it.
         * This is necessary to render the to/from addresses of transactions in a user interface.
         * The official client does something similar.
         */
        private void parse(byte[] programBytes, int offset, int length)
        {
            // TODO: this is inefficient
            programCopy = new byte[length];
            Array.Copy(programBytes, offset, programCopy, 0, length);

            program = programCopy;
            offset = 0;
            chunks = new List<byte[]>(10);  // Arbitrary choice of initial size.
            cursor = offset;
            while (cursor < offset + length)
            {
                int opcode = readByte();
                if (opcode >= 0xF0)
                {
                    // Not a single byte opcode.
                    opcode = (opcode << 8) | readByte();
                }

                if (opcode > 0 && opcode < OP_PUSHDATA1)
                {
                    // Read some bytes of data, where how many is the opcode value itself.
                    chunks.Add(getData(opcode));  // opcode == len here.
                }
                else if (opcode == OP_PUSHDATA1)
                {
                    int len = readByte();
                    chunks.Add(getData(len));
                }
                else if (opcode == OP_PUSHDATA2)
                {
                    // Read a short, then read that many bytes of data.
                    int len = readByte() | (readByte() << 8);
                    chunks.Add(getData(len));
                }
                else if (opcode == OP_PUSHDATA4)
                {
                    // Read a uint32, then read that many bytes of data.
                    // TODO: log.error("PUSHDATA4: Unimplemented");
                }
                else
                {
                    chunks.Add(new byte[] { (byte)opcode });
                }
            }
        }

        /**
         * Returns true if this transaction is of a format that means it was a direct IP to IP transaction. These
         * transactions are deprecated and no longer used, support for creating them has been removed from the official
         * client.
         */
        public bool isSentToIP()
        {
            if (chunks.Count != 2)
                return false;
            return (0xFF & chunks[1][0]) == OP_CHECKSIG && chunks[0].Length > 1;
        }

        /**
         * If a program matches the standard template DUP HASH160 <pubkey hash> EQUALVERIFY CHECKSIG
         * then this function retrieves the third element, otherwise it throws a ScriptException.
         *
         * This is useful for fetching the destination address of a transaction.
         */
        public byte[] getPubKeyHash()
        {
            if (chunks.Count != 5)
                throw new Exception("Script not of right size to be a scriptPubKey, " +
                                          "expecting 5 but got " + chunks.Count);
            if ((0xFF & chunks[0][0]) != OP_DUP ||
                (0xFF & chunks[1][0]) != OP_HASH160 ||
                (0xFF & chunks[3][0]) != OP_EQUALVERIFY ||
                (0xFF & chunks[4][0]) != OP_CHECKSIG)
                throw new Exception("Script not in the standard scriptPubKey form");

            // Otherwise, the third element is the hash of the public key, ie the bitcoin address.
            return chunks[2];
        }

        /**
         * If a program has two data buffers (constants) and nothing else, the second one is returned.
         * For a scriptSig this should be the public key of the sender.
         *
         * This is useful for fetching the source address of a transaction.
         */
        public byte[] getPubKey()
        {
            if (chunks.Count == 1)
            {
                // Direct IP to IP transactions only have the public key in their scriptSig.
                return chunks[0];
            }
            if (chunks.Count != 2)
                throw new Exception("Script not of right size to be a scriptSig, expecting 2" +
                                           " but got " + chunks.Count);
            if (!(chunks[0].Length > 1) && (chunks[1].Length > 1))
                throw new Exception("Script not in the standard scriptSig form: " +
                chunks.Count + " chunks");
            return chunks[1];
        }

        /**
         * Convenience wrapper around getPubKey. Only works for scriptSigs.
         */
        public Address getFromAddress()
        {
            return new Address(param, Utils.sha256hash160(getPubKey()));
        }


        /**
         * Gets the destination address from this script, if it's in the required form (see getPubKey).
         * @throws ScriptException
         */
        public Address getToAddress() 
    {
        return new Address(param, getPubKeyHash());
    }

        /**
         * Runs the script with the given Transaction as the "context". Some operations like CHECKSIG
         * require a transaction to operate on (eg to hash). The context transaction is typically
         * the transaction having its inputs verified, ie the one where the scriptSig comes from.
         */
        public bool run(Transaction context)
        {
            foreach (byte[] chunk in chunks)
            {
                if (chunk.Length == 1)
                {
                    int opcode = 0xFF & chunk[0];
                    switch (opcode)
                    {
                        case OP_DUP:
                            opDup();
                            break;
                        case OP_HASH160:
                            opHash160();
                            break;
                        case OP_EQUALVERIFY:
                            opEqualVerify();
                            break;
                        case OP_CHECKSIG:
                            //opCheckSig(context);
                            throw new Exception("NYI");
                            //break;
                        default:
                        // TODO: log.debug("Unknown/unimplemented opcode: {}", opcode);
                            break;
                    }
                }
                else
                {
                    // Data block, push it onto the stack.
                    // TODO: log.debug("Push {}", Utils.bytesToHexString(chunk));
                    stack.Push(chunk);
                }
            }
            byte[] result = stack.Pop();
            if (result.Length != 1)
                throw new Exception("Script left junk at the top of the stack: " + Utils.bytesToHexString(result));
            return result[0] == 1;
        }

        void logStack()
        {
            for (int i = 0; i < stack.Count; i++)
            {
                // TODO: log.debug("Stack[{}]: {}",i , Utils.bytesToHexString(stack[i]));
            }
        }

        // WARNING: Unfinished and untested!
        //@SuppressWarnings("unused")
        //private void opCheckSig( Transaction context) throws ScriptException {
        //    byte[] pubkey = stack.pop();
        //    byte[] sigAndHashType = stack.pop();
        //    // The signature has an extra byte on the end to indicate the type of hash. The signature
        //    // is over the contents of the program, minus the signature itself of course.
        //    byte hashType = sigAndHashType[sigAndHashType.length - 1];
        //    // The high bit of the hashType byte is set to indicate "anyone can pay".
        //    bool anyoneCanPay = hashType < 0;
        //    // Mask out the top bit.
        //    hashType &= (byte)-1 >>> 1;
        //    Transaction.SigHash sigHash;
        //    switch (hashType) {
        //    case 1: sigHash = SigHash.ALL; break;
        //    case 2: sigHash = SigHash.NONE; break;
        //    case 3: sigHash = SigHash.SINGLE; break;
        //    default:
        //        // TODO: This should probably not be an exception.
        //        throw new ScriptException("Unknown sighash byte: " + sigAndHashType[sigAndHashType.length - 1]);
        //    }

        //    byte[] sig = new byte[sigAndHashType.length - 1];
        //    System.arraycopy(sigAndHashType, 0, sig, 0, sig.length);

        //    log.debug("CHECKSIG: hashtype={} anyoneCanPay={}", sigHash, anyoneCanPay);
        //    if (context == null) {
        //        // TODO: Fix the unit tests to run scripts in transaction context then remove this.
        //        pushBool(true);
        //        return;
        //    }
        //    // TODO: Implement me!
        //    // Transaction tx = context.simplify(sigHash, 0, anyoneCanPay);

        //    // The steps to do so are as follows:
        //    //   - Use the hashtype to fiddle the transaction as appropriate
        //    //   - Serialize the transaction and hash it
        //    //   - Use EC code to verify the hash matches the signature
        //    pushBool(true);
        //}

        private void pushBool(bool val)
        {
            stack.Push(new byte[] { val ? (byte)1 : (byte)0 });
        }

        private void opEqualVerify()
        {
            // TODO: log.debug("EQUALVERIFY");
            byte[] a = stack.Pop();
            byte[] b = stack.Pop();
            if (!Array.Equals(a, b))
                throw new Exception("EQUALVERIFY failed: " + Utils.bytesToHexString(a) + " vs " +
                        Utils.bytesToHexString(b));
        }

        /** Replaces the top item in the stack with a hash160 of it */
        private void opHash160()
        {
            byte[] buf = stack.Pop();
            byte[] hash = Utils.sha256hash160(buf);
            stack.Push(hash);
            // TODO: log.debug("HASH160: output is {}", Utils.bytesToHexString(hash));
        }

        /** Duplicates the top item on the stack */
        private void opDup()
        {
            // TODO: log.debug("DUP");
            //stack.Push(Array.clone(stack.lastElement()));
            stack.Push(stack.Last());   // TODO: ok? (see row above)
        }

        ////////////////////// Interface for writing scripts from scratch ////////////////////////////////

        /** Writes out the given byte buffer to the output stream with the correct opcode prefix */
        internal static void writeBytes(Stream os, byte[] buf)
        {
            if (buf.Length < OP_PUSHDATA1)
            {
                os.WriteByte((byte)buf.Length);
                os.Write(buf, 0, buf.Length);
            }
            else if (buf.Length < 256)
            {
                os.WriteByte((byte)OP_PUSHDATA1);
                os.WriteByte((byte)buf.Length);
                os.Write(buf, 0, buf.Length);
            }
            else if (buf.Length < 65536)
            {
                os.WriteByte((byte)OP_PUSHDATA2);
                os.WriteByte((byte)(0xFF & (buf.Length)));
                os.WriteByte((byte)(0xFF & (buf.Length >> 8)));
            }
            else
            {
                throw new Exception("Unimplemented");
            }
        }

        public static byte[] createOutputScript(Address to)
        {
            try
            {
                // TODO: Do this by creating a Script *first* then having the script reassemble itself into bytes.
                MemoryStream bits = new MemoryStream();
                bits.WriteByte((byte)OP_DUP);
                bits.WriteByte((byte)OP_HASH160);
                writeBytes(bits, to.getHash160());
                bits.WriteByte((byte)OP_EQUALVERIFY);
                bits.WriteByte((byte)OP_CHECKSIG);
                return bits.ToArray();
            }
            catch (IOException e)
            {
                throw e;  // Cannot happen.
            }
        }

        internal static byte[] createInputScript(byte[] signature, byte[] pubkey)
        {
            try
            {
                // TODO: Do this by creating a Script *first* then having the script reassemble itself into bytes.
                MemoryStream bits = new MemoryStream();
                writeBytes(bits, signature);
                writeBytes(bits, pubkey);
                return bits.ToArray();
            }
            catch (IOException e)
            {
                throw e;
            }
        }
    }
}
